#include<iostream>
#include<cstdlib>
#include<unistd.h>
#include<errno.h>
#include<cstring>
#include<sys/types.h>
#include<sys/wait.h>
#include<string>
#include <sys/stat.h>
#include <fcntl.h>


#define ENV_MAX 64
char* g_env[ENV_MAX];

extern char** environ;//总是指向bash的环境表, 在系统启动的时候就导入了
void init_env()
{
    int i = 0;
    for(; environ[i]; ++i)
    {
        g_env[i] = (char*)malloc(strlen(environ[i]) + 1);//+1是为了存'\n', strlen不会记录'\0'
        memcpy(g_env[i], environ[i], sizeof(g_env[i]));
    }  
    g_env[i] = nullptr;//最后一位必须是nullptr
}


//否则常量字符串"NONE"返回时会权限放大
const char* get_USER()
{
    char* name = getenv("USER");
    return name == nullptr ? "NONE" : name;
}


//不知道为什么 取不出来HOSTNAME
const char* get_HOSTNAME()
{
    char* hostname = getenv("HOSTNAME");
    return hostname == nullptr ? "NONE" : hostname;
}

// const char* get_HOSTNAME()
// {
//     char hostname[255];
//     int get_ret = gethostname(hostname, sizeof(hostname));

//     if (get_ret == -1)
//         return "NONE";
//     else
//         return hostname;
// }

#define CWD_MAX 1024
char cwd[CWD_MAX];
char cwd_env[CWD_MAX];

const char* get_PWD()//工作目录先变化, shell再自己自动更新环境变量PATH -> 所以MyShell要手动实现该过程
{

    char* ret = getcwd(cwd, sizeof(cwd));
    if(ret == nullptr)
        exit(errno);
    
    //更新环境变量: 以"xx=xxx"的格式
    snprintf(cwd_env, sizeof(cwd_env), "PWD=%s", cwd);
    putenv(cwd_env);
    
    return cwd;
}

const char* get_HOME()
{
    char* home = getenv("HOME");
    return home;
}


//1.输出命令行提示字符串
void print_command_prompt()
{
//提示格式
#define PROMPT_FORMAT "[%s@%s:%s]$ "

    printf(PROMPT_FORMAT, (const char*)get_USER(), (const char*)get_HOSTNAME(), (const char*)get_PWD());
    fflush(stdout);//刷新缓冲区到标准输出流(屏幕) -- 保险工作: 确保刷新成功
}



//2.获取用户输入的指令
bool get_user_command(char* command_line, size_t str_max_size)
{
    char* ret = fgets(command_line, str_max_size, stdin);//网站看函数使用
    if(ret == nullptr)
        exit(errno);
    command_line[strlen(command_line) - 1] = '\0';//清除最后的'\n', 不用考虑越界 因为fget函数至少读到一个'\n'
    if(strlen(command_line) == 0)
        return false;
    return true;
}


enum dir_type
{
    NoneRedir = 1,
    InputRedir = 2,
    OutputRedir = 3,
    AppOutputRedir = 4
};

int redir = NoneRedir;
char* filename = nullptr;

char* skip_space(char* local_filename, const char* p_end)
{
    while( (local_filename != p_end) && (*(local_filename) == ' ') )
    {
        ++local_filename; //不能放外面, 会多加一次
    }
    
    return local_filename;
}

//3.重定向分析: "ls > test.txt" 
void redir_parse(char* command_line)
{
    redir = NoneRedir;
    filename = nullptr;

    int end = strlen(command_line);
    int start = end;
    while(start >= 0)
    {
        if(start == 13)
        {
            int x = 10;
        }

        if(command_line[start] == '<')
        {
            redir = InputRedir;
            command_line[start] = '\0';//裁剪命令
            filename = skip_space(&command_line[start + 1], &command_line[end]);
            if(filename == &command_line[end - 1])
            {
                printf(">>错误!! 没有重定向后的文件!!\n");
                exit(2);
            }
            break;
        }
        else if(command_line[start] == '>')
        {                                               //  ↓end
            if(start > 0 && command_line[start - 1] == '>') // >>
            {
                redir = AppOutputRedir;
                command_line[start - 1] = '\0';//裁剪命令 -- 记得是把左边的'>'置为结束符
                command_line[start] = '\0';//无所谓
                filename = skip_space(&command_line[start + 1], &command_line[end]);
                if(filename == &command_line[end - 1])
                {
                    printf(">>错误!! 没有重定向后的文件!!\n");
                    exit(2);
                }
                break;
            }
            else // >
            {
                redir = OutputRedir;
                command_line[start] = '\0';//裁剪命令
                filename = skip_space(&command_line[start + 1], &command_line[end]);
                if(filename == &command_line[end - 1])
                {
                    printf(">>错误!! 没有重定向后的文件!!\n");
                    exit(2);
                }
                break;
            }
        }
        --start;
    }

}



void do_redir()
{
    if(redir == InputRedir)
    {
        int fd = open(filename, O_RDONLY);
        if(fd < 0)
            exit(88);//打开失败
        dup2(fd, 0);
    }

    else if(redir == OutputRedir)
    {
        int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if(fd < 0)
            exit(89);//打开失败
        dup2(fd, 1);
    }

    if(redir == InputRedir)
    {
        int fd = open(filename, O_APPEND | O_CREAT | O_TRUNC, 0666);
        if(fd < 0)
            exit(90);//打开失败
        dup2(fd, 1);
    }
}




//全局变量
#define ARGC_MAX 64
int g_argc;
char* g_argv[ARGC_MAX];

//4.解析指令(切分命令 形成命令行参数表)
bool command_parse(char* command_line)
{
#define DELIMITER " "

    g_argc = 0;
    g_argv[g_argc++] = strtok(command_line, DELIMITER);//分隔符: char* -> 双引号" "
    //每次只能分割一下, 后续用nullptr代表从上次分割的位置继续分割
    while( (g_argv[g_argc++] = strtok(nullptr, DELIMITER)) != nullptr);

    g_argv[g_argc] == nullptr;//最后一个元素必须置为空
    g_argc--;//修正 -- 最后一次 strtok从'\0'开始分隔导致分割失败 但g_argc也会++

    return g_argc > 0 ? true : false;//加层保险
}



int last_exit_code = 0;//用于echo $?
 
//5.检测并处理内建命令(bash自己执行 -- 本质: bash调用自己的函数)
bool check_and_execute_command_bulitin()
{
    //内建命令要重定向, 需要在你完成指令后复原三个标准流的文件描述符(在替换之前, 给你要换的标准流建一个临时拷贝, 即让它指向一个新的描述符并且记录该描述符), 其他流程和外部命令一样
    //这里就不实现了

    //cd
    if(strcmp(g_argv[0], "cd") == 0)
    { 
        int ch_ret = 0;
        if(g_argc == 1) //"cd" -> 返回家目录
            ch_ret = chdir(get_HOME()); //改变当前进程的工作目录 -- 工作目录先变化, shell再自己更新环境变量PATH
        
        else if (g_argc == 2) //"cd -" "cd ~"等特殊命令暂不实现
            ch_ret = chdir(g_argv[1]);

        if(g_argc > 2 || ch_ret == -1)
            printf(">>cd命令参数输入有误, 重新输入!!\n");

        return true;
    }

    //echo
    else if(strcmp(g_argv[0], "echo") == 0)
    {
        //只实现"echo $?", "echo $env", "echo XXX"
        if(g_argc == 2)
        {
            if(g_argv[1][0] == '$')
            {
                if(g_argv[1][1] == '?')
                {
                    printf("%d\n", last_exit_code);
                    last_exit_code = 0;
                }
                else
                {
                    std::string str(g_argv[1]);
                    std::string option = str.substr(1);
                    char* env = getenv(option.c_str());
                    if(env)
                        printf("%s\n", env);

                    else
                        std::cout << ">>功能暂未实现哦~~" << std::endl;
                    
                }
            }
            else
            {
                std::cout << g_argv[1] << std::endl;
            }
            return true;//子进程也能实现echo, bash实现了两份echo
        }
    }

    //pwd,export,env......


    return false;
}


//6.执行外部命令(交给子进程执行)
void execute_command()
{
    pid_t id = fork();
    if(id == -1)
        exit(errno);

    if(id == 0) //child
    {
        if(filename)
            do_redir();

        //进程替换不影响重定向(替换只改页表和代码数据, 不动pcb中的这些表, 包括文件描述符表)
        execvp(g_argv[0], g_argv);
    }
    
    //father 
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        if(WIFEXITED(status))
            last_exit_code = WEXITSTATUS(status);
    }
} 

void print_argv()
{
    for(int i = 0; i < g_argc; ++i)
    {
        std::cout << g_argv[i] << std::endl;
    }
    printf("%d\n", g_argc);
}


int main()
{
    //0.从配置文件中读取出环境变量表
    //我实现MyShell的env表是从父进程Shell继承而来的, 这里就不实现导入环境变量了
    init_env();


    while(1)
    {
        //1.输出命令行提示字符串
        print_command_prompt();

        //2.获取用户输入的指令
#define COMMAND_MAX_SIZE 1024

        char command_line[COMMAND_MAX_SIZE];
        if(get_user_command(command_line, sizeof(command_line)) == false)
            continue;


        //3.重定向分析
        redir_parse(command_line);
        //printf("%s\n%s\n%d", command_line, filename, redir);
        

        //4.解析指令(切分命令 形成命令行参数表)
        if((command_parse(command_line)) == false)
            continue;
        //print_argv();


        //5.检测并处理内建命令(bash自己执行)
        if((check_and_execute_command_bulitin()) == true)
            continue;

        //6.执行外部命令(交给子进程执行)
        execute_command();
        
    }

    return 0;
}